package pap.test.fcPuCc;

import gov.nasa.jpf.jvm.Verify;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class CollisionChecker extends Task {

	java.util.concurrent.ConcurrentHashMap<Body, Collision> collisionMap;
	volatile int checkedBodies = 0;
	static AtomicInteger iterCounter;
	int iterations;

	SynchronizedCounter bodiesToProcessCounter, forceCalculatorCounter;
	List<Body> bodiesList;

	public CollisionChecker(int threadNum, List<Body> bodiesList,
			SynchronizedCounter bodiesToProcessCounter,
			SynchronizedCounter forceCalculatorCounter, int iterations) {
		super(threadNum);

		Verify.beginAtomic();

		this.bodiesList = bodiesList;
		collisionMap = new ConcurrentHashMap<Body, Collision>();
		this.bodiesToProcessCounter = bodiesToProcessCounter;
		this.forceCalculatorCounter = forceCalculatorCounter;
		this.iterCounter = new AtomicInteger(0);
		this.iterations = iterations;
		Verify.endAtomic();
	}

	@Override
	protected void mainTask() throws InterruptedException {
		int index;

		while (iterCounter.getAndIncrement() < iterations) {
			index = getNextBodyIndex();

			ccRunning.incrementAndGet();

			assert (fcRunning.get() == 0 && puRunning.get() == 0);

			checkBodyCollision(index);

			increaseCheckedBodiesNumber();
		}

	}

	private int getNextBodyIndex() throws InterruptedException {
		return bodiesToProcessCounter.getNextVal();
	}

	/**
	 * Checks if the body with index bodyIndex collides with the bodies that
	 * have a greater index.
	 * 
	 * @param bodyIndex
	 */
	private void checkBodyCollision(int bodyIndex) {

		Body body = bodiesList.get(bodyIndex);

		for (int i = bodyIndex + 1; i < bodiesList.size(); i++) {
			checkAndManageCollision(body, bodiesList.get(i));
		}

	}

	/**
	 * Checks if body1 collides with body2.
	 * 
	 * @param bodyIndex1
	 * @param bodyIndex2
	 */
	private void checkAndManageCollision(Body body1, Body body2) {
		if (body1.intersect(body2)) {
			addCollision(body1, body2);
		}
	}

	/*
	 * When happen a collision between two bodies is necessary to update the map
	 * containing the collision's list
	 */
	private void addCollision(Body bodyIndex1, Body bodyIndex2) {

		Collision collision;
		/*
		 * If a body collided yet, the collision will be in the map, so it's
		 * only necessary to add the index of the other body to the collision
		 * and to the map. Else if for both the body this is the first collision
		 * (of this step), it's necessary to create a new collision instance,
		 * and add it to the map, associated with either the first and the
		 * second index
		 */
		Collision body1Collision = collisionMap.get(bodyIndex1);
		Collision body2Collision = collisionMap.get(bodyIndex2);

		if (body1Collision != null) {
			if (body2Collision == null) {
				collision = body1Collision;
				collision.addCollidedBody(bodyIndex2);
				collisionMap.put(bodyIndex2, collision);
			}
			// Extremely rare case: body1 & body2 collided with other bodies,
			// but are not in the same collision
			else if (body2Collision != body1Collision) {
				// Fuse the collision adding all the bodies of collision2, to
				// collision1
				for (Body body : body2Collision.collidedBodies) {
					// Change the collision for every body of the collision2
					collisionMap.put(body, body1Collision);
					body1Collision.addCollidedBody(body);
				}
			}
		} else if (body2Collision != null) {
			if (body1Collision == null) {
				collision = body2Collision;
				collision.addCollidedBody(bodyIndex1);
				collisionMap.put(bodyIndex1, collision);
			}
		} else {
			collision = new Collision(bodyIndex1, bodyIndex2);
			collisionMap.put(bodyIndex1, collision);
			collisionMap.put(bodyIndex2, collision);
		}
	}

	/*
	 * Every time a body is checked is necessary for the executor to notify the
	 * completion. The value of the counter 'checkedBodies' is incremented by
	 * one, allowing to know when all bodies have been checked. After that is
	 * necessary to wake the component ForceCalculator.
	 */
	private synchronized void increaseCheckedBodiesNumber() {

		checkedBodies++;
		
		ccRunning.decrementAndGet();
		/*
		 * If all bodies have been checked, the counter must be reset, since
		 * every executor will be blocked on the queue while a full step will be
		 * completed
		 */
		if (checkedBodies >= bodiesList.size()) {
			// The last executor thread must execute the fusion operation
			fuseCollidedBodies();

			// The collisions map must be reset
			collisionMap = new ConcurrentHashMap<Body, CollisionChecker.Collision>();

			checkedBodies = 0;

			wakeForceCalculator();
		}

	}

	/**
	 * Operation executed by a single thread, that fuses together all the
	 * collided bodies
	 * 
	 * @throws UncompatibleDimensionsException
	 */
	private void fuseCollidedBodies() {
		Collision[] collisions = getCollisionsWithoutDuplicated();
		List<Body> collidedBodies;

		for (Collision collision : collisions) {
			collidedBodies = collision.getCollidedBodies();

			for (Body body : collidedBodies) {
				bodiesList.remove(body);
			}
			bodiesList.add(new CompoundBody(collidedBodies));
		}

	}

	/**
	 * Returns an array containing every collision happened only one time
	 * 
	 * @return
	 */
	private Collision[] getCollisionsWithoutDuplicated() {
		return new HashSet<Collision>(collisionMap.values())
				.toArray(new Collision[0]);
	}

	private void wakeForceCalculator() {
		forceCalculatorCounter.reset(bodiesList.size());

	}

	private class Collision {
		List<Body> collidedBodies;

		public Collision(Body body1, Body body2) {
			collidedBodies = new ArrayList<Body>();
			collidedBodies.add(body1);
			collidedBodies.add(body2);
		}

		public void addCollidedBody(Body body) {
			collidedBodies.add(body);
		}

		public List<Body> getCollidedBodies() {
			return collidedBodies;
		}
	}
}